Skip to content

fix(sso): scope redirect loop counter to SSO hops#1302

Merged
superdav42 merged 1 commit into
mainfrom
feature/auto-20260527-191135-gh1299
May 28, 2026
Merged

fix(sso): scope redirect loop counter to SSO hops#1302
superdav42 merged 1 commit into
mainfrom
feature/auto-20260527-191135-gh1299

Conversation

@superdav42

@superdav42 superdav42 commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Scoped the SSO redirect-loop counter to requests with an SSO fingerprint.
  • Preserved loop protection for SSO query parameters and main-site /sso referers.
  • Added coverage proving plain logged-out protected URL visits do not consume the redirect budget.

Testing

  • WP_TESTS_DIR=/tmp/wordpress-tests-lib vendor/bin/phpunit --filter SSO_Test
  • vendor/bin/phpcs inc/sso/class-sso.php tests/WP_Ultimo/SSO/SSO_Test.php (passes with existing test warnings for file_get_contents())

MERGE_SUMMARY: Scoped should_skip_redirect_due_to_loop() so only SSO-fingerprinted requests consume the wu_sso_redirect_attempts budget, with PHPUnit coverage for non-SSO requests and SSO referers.

Resolves #1299

Summary by CodeRabbit

  • Bug Fixes

    • Improved Single Sign-On (SSO) redirect-loop detection to accurately identify actual SSO authentication attempts, reducing false positives that previously blocked legitimate SSO sessions.
  • Tests

    • Added test coverage for redirect-loop counter behavior with SSO and non-SSO request scenarios.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR refines SSO redirect-loop detection to scope the counter only to actual SSO loop hops. A new is_sso_loop_request() helper identifies SSO requests by checking for wu_sso_token, SSO query params, or matching HTTP referer path. The gate in should_skip_redirect_due_to_loop() now applies the threshold only when the request has SSO fingerprints, preventing benign auth redirects from consuming the throttle budget.

Changes

SSO loop detection

Layer / File(s) Summary
SSO loop detection logic
inc/sso/class-sso.php
A new private is_sso_loop_request() helper detects SSO fingerprint by checking for wu_sso_token, SSO query params (sso=login, return_url, _jsonp), or matching the HTTP referer path against the configured SSO path. The should_skip_redirect_due_to_loop() gate now applies the redirect-loop threshold only when is_sso_loop_request() confirms the request is an SSO loop hop.
Redirect-loop counter tests
tests/WP_Ultimo/SSO/SSO_Test.php
Test tearDown() now clears $_REQUEST['sso'], $_REQUEST['_jsonp'], and $_SERVER['HTTP_REFERER'] to prevent cross-test contamination. Two existing redirect-loop counter tests are updated to set $_REQUEST['sso'] = 'login' for correct SSO classification. Two new tests verify the refined behavior: one confirms non-SSO requests do not increment the counter, another confirms SSO referer correctly consumes the budget.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

status:available, review-feedback-scanned

Poem

🐰 A loop was bumping far too fast,

Counting guests who'd never passed

Through SSO gates before.

Now fingerprints check who's at the door—

Real hops counted, rest ignored! 🔐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: scoping the redirect-loop counter to only count SSO hops rather than all protected URL visits.
Linked Issues check ✅ Passed The code changes fully implement the objectives from issue #1299 by adding SSO fingerprint detection and only incrementing the redirect counter for actual SSO requests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the redirect-loop counter behavior and adding corresponding PHPUnit test coverage as outlined in the linked issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/auto-20260527-191135-gh1299

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
inc/sso/class-sso.php (1)

441-441: ⚡ Quick win

Use Yoda comparisons in this production conditional.

Please switch the comparisons at Line 441 to Yoda style for consistency with repository standards.

Proposed fix
-		if ($threshold < 1 || $window < 1 || ! $this->is_sso_loop_request()) {
+		if (1 > $threshold || 1 > $window || ! $this->is_sso_loop_request()) {
			return false;
		}

As per coding guidelines: "Use Yoda conditions in production code (e.g., 'value' === $var, not $var === 'value')"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/sso/class-sso.php` at line 441, Change the conditional to use Yoda
comparisons: replace "if ($threshold < 1 || $window < 1 || !
$this->is_sso_loop_request())" with a Yoda-style form such as "if (1 >
$threshold || 1 > $window || false === $this->is_sso_loop_request())" so the
constants come first; update this inside the same method in class-sso.php where
$threshold, $window and the is_sso_loop_request() call are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/sso/class-sso.php`:
- Around line 491-494: The referer check is too permissive because it only
compares $referer_path to $sso_path; update the logic in class-sso.php so it
validates both host and a strict endpoint boundary: parse the referer host (e.g.
$referer_host = (string) wp_parse_url($referer, PHP_URL_HOST)) and compare it to
the site/home host, then ensure the path exactly matches $sso_path or starts
with $sso_path followed by '/' (to avoid matching paths like '/ssohijack'); use
$this->get_url_path(), $referer_path and wp_parse_url to implement these two
checks and only return true when both host and bounded-path conditions are met.

---

Nitpick comments:
In `@inc/sso/class-sso.php`:
- Line 441: Change the conditional to use Yoda comparisons: replace "if
($threshold < 1 || $window < 1 || ! $this->is_sso_loop_request())" with a
Yoda-style form such as "if (1 > $threshold || 1 > $window || false ===
$this->is_sso_loop_request())" so the constants come first; update this inside
the same method in class-sso.php where $threshold, $window and the
is_sso_loop_request() call are used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: effcff6f-f8cb-4b91-a213-7c2ebce5c7ef

📥 Commits

Reviewing files that changed from the base of the PR and between 050d819 and ea57edf.

📒 Files selected for processing (2)
  • inc/sso/class-sso.php
  • tests/WP_Ultimo/SSO/SSO_Test.php

Comment thread inc/sso/class-sso.php
Comment on lines +491 to +494
$referer_path = (string) wp_parse_url($referer, PHP_URL_PATH);
$sso_path = '/' . ltrim($this->get_url_path(), '/');

return 0 === strpos($referer_path, $sso_path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten referer fingerprint to main-site /sso endpoint only.

Line 494 currently treats any referer path starting with /sso as an SSO hop, regardless of host and endpoint boundary. That can still consume wu_sso_redirect_attempts for non-SSO traffic.

Proposed fix
-		$referer_path = (string) wp_parse_url($referer, PHP_URL_PATH);
-		$sso_path     = '/' . ltrim($this->get_url_path(), '/');
-
-		return 0 === strpos($referer_path, $sso_path);
+		$referer_host = strtolower((string) wp_parse_url($referer, PHP_URL_HOST));
+		$referer_path = (string) wp_parse_url($referer, PHP_URL_PATH);
+		$main_host    = strtolower((string) wp_parse_url(get_home_url(wu_get_main_site_id()), PHP_URL_HOST));
+		$sso_path     = '/' . ltrim($this->get_url_path(), '/');
+
+		if (empty($referer_host) || $referer_host !== $main_host) {
+			return false;
+		}
+
+		return 1 === preg_match('#^' . preg_quote($sso_path, '#') . '(?:/|$)#', $referer_path);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/sso/class-sso.php` around lines 491 - 494, The referer check is too
permissive because it only compares $referer_path to $sso_path; update the logic
in class-sso.php so it validates both host and a strict endpoint boundary: parse
the referer host (e.g. $referer_host = (string) wp_parse_url($referer,
PHP_URL_HOST)) and compare it to the site/home host, then ensure the path
exactly matches $sso_path or starts with $sso_path followed by '/' (to avoid
matching paths like '/ssohijack'); use $this->get_url_path(), $referer_path and
wp_parse_url to implement these two checks and only return true when both host
and bounded-path conditions are met.

@superdav42 superdav42 merged commit 80d285f into main May 28, 2026
8 of 11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Summary

  • Scoped the SSO redirect-loop counter to requests with an SSO fingerprint.
  • Preserved loop protection for SSO query parameters and main-site /sso referers.
  • Added coverage proving plain logged-out protected URL visits do not consume the redirect budget.

Testing

  • WP_TESTS_DIR=/tmp/wordpress-tests-lib vendor/bin/phpunit --filter SSO_Test
  • vendor/bin/phpcs inc/sso/class-sso.php tests/WP_Ultimo/SSO/SSO_Test.php (passes with existing test warnings for file_get_contents())
    MERGE_SUMMARY: Scoped should_skip_redirect_due_to_loop() so only SSO-fingerprinted requests consume the wu_sso_redirect_attempts budget, with PHPUnit coverage for non-SSO requests and SSO referers.

Merged via PR #1302 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).


aidevops.sh v3.20.2 spent 38s on this as a headless bash routine.

@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label May 28, 2026
superdav42 added a commit that referenced this pull request May 29, 2026
…1309)

The unattached-broker JSONP branch in handle_broker() returned
'{code: 0, message: "Broker not attached"}', which sso.js treats
as a denial (assets/js/sso.js lines 101-117). That set the
wu_sso_denied cookie for 5 minutes on the very first subsite
front-end page-load, silently disabling auto-SSO before it could
complete a single round-trip — even when the user was in fact
logged in on the main site.

The denial payload is the wrong signal here: the broker not being
attached on the JSONP probe is the expected state on the first
hit, not a failure. Return 'verify: must-redirect' instead so the
already-existing sso.js branch (assets/js/sso.js lines 93-95)
performs a top-level navigation to '<broker>/sso?return_url=...'.
That request re-enters handle_broker() as a regular page load and
falls through to the non-JSONP redirect, which after #1301 reaches
the cookie-less server handler via getAttachUrl() and completes the
SSO round-trip.

This is the minimal subset of #1297 that the post-#1301/#1302
codebase still needs. The /sso-grant routing fix (#1301) and the
scoped redirect-loop counter (#1302) already shipped, so the rest
of #1297's PHP changes are no longer required.

Regression tests in tests/WP_Ultimo/SSO/SSO_Test.php:
- test_handle_broker_source_jsonp_unattached_returns_must_redirect:
  locks in the new payload and forbids the legacy 'Broker not
  attached' denial.
- test_sso_js_handles_must_redirect_verify: confirms sso.js still
  recognises the must-redirect verify value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSO: should_skip_redirect_due_to_loop() over-counts on non-SSO requests

1 participant